title: Chart: gesture, overlay & styling description: ChartGesture (custom gestures + ChartProxy.select* for programmatic selection), ChartPlotStyle (plot-area background / border / frame builder), ChartAxisLabelFormat (currency / number / date precision factories), and per-mark VoiceOver fields (accessibilityLabel / Value / Hidden).
This example demonstrates four chart interaction / customization features:
ChartOverlay— a reader-style child of<Chart>that gives custom content access to aChartProxyfor hit-testing, value↔coordinate conversion, and reading the plot area frame. Mirrors SwiftUI Charts'chartOverlay(alignment:content:) { proxy in ... }.- Range selection — pass
from/to(instead ofvalue) tochartXSelection/chartYSelectionand the bridge wires up SwiftUI'schartXSelection(range:)/chartYSelection(range:)overload. ChartGesture— another reader-style child of<Chart>. The closure returns anyGesturedescriptor and receives a writableChartProxyso you can drive selection with custom gestures (e.g. single-finger drag-range on a categorical String axis). MirrorschartGesture(_:) { proxy in ... }.ChartPlotStyle— a reader-style child whose closure builds a chain of plot-area modifiers (background / border / frame / shadow / corner radius / clip shape / opacity). MirrorschartPlotStyle { plot in plot.background(...).border(...) }.
ChartOverlay quick reference
ChartProxy is synchronous and returns null when the type token does not match the chart's actual axis data type:
Range selection quick reference
- The bridge dispatches on the presence of
from/toto pick the SwiftUIchartXSelection(range:)overload. Single-value selection (value+onChanged) keeps working unchanged. valueType: 'string' | 'number' | 'date'— must match the chart's plotted axis data type.onChangedfires whenever the selection changes and again withnullwhen the selection is cleared.
Axis-type constraint: range selection only works on continuous axes (number / date). On categorical String axes SwiftUI Charts neither responds to the default range gesture nor reverse-maps screen-pixel coordinates back to a category, so even
<ChartGesture>+proxy.selectXRange(...)cannot drive a String-axis range. For String axes use the single-value form (ChartSelection) instead.
Activation gesture (platform-specific)
chartXSelection(range:) default gesture differs by platform — this is a SwiftUI Charts SDK behaviour, not a bridge limitation:
- iOS: a two-finger tap on the chart. In iOS Simulator, hold ⌥ Option while clicking the chart to simulate a two-finger touch.
- macOS: a drag gesture.
Single-finger long-press-and-drag does NOT trigger range selection by default. If you need a single-finger interaction or any custom activation, use <ChartGesture> to take over the chart's gesture handling.
Sources: Mastering charts in SwiftUI · Selection, WWDC23 · Explore pie charts and interactivity in Swift Charts.
The two forms are mutually exclusive on a given axis. Use a single-value selection for tap interactions, and the range form for drag-to-zoom or drag-to-summarize gestures.
Axis-label precision (ChartAxisLabelFormat)
In addition to the short string tokens ('number' | 'percent' | 'currency' | 'date' | 'time' | 'dateTime'), the valueLabel.format field of chartXAxis / chartYAxis accepts a native ChartAxisLabelFormat instance. Use it when you need fraction-digit precision, a fixed currency code, or a non-default date / time style — mirrors SwiftUI Foundation's FormatStyle family.
Available factories:
Short string tokens stay fully supported; pick whichever fits. Use the class when you need precision / currency / style; otherwise the concise
format: 'number'form is plenty.
ChartGesture quick reference
- The closure returns a
Gesturedescriptor (DragGesture()/TapGesture()/LongPressGesture()/MagnifyGesture()/RotateGesture()), equivalent to SwiftUI'schartGesture { proxy in ... }. proxy.selectXRange / selectYRange / selectXValue / selectYValue / selectAngleValueaccept screen-space pixel coordinates (not data values) — feedDragGestureeventstartLocation.x/location.xdirectly without reverse-mapping.- After writing the selection, the matching
chartXSelection / chartYSelection / chartAngleSelectionbinding firesonChangedwith the bound data values. - Only the first
<ChartGesture>child of a chart is used (same rule as<ChartOverlay>). - Use this to replace the default gesture (single-finger drag, custom activation, etc.).
- Axis-type constraint: same as the default range gesture — only number / date axes are supported. On categorical String axes the SDK can't reverse-map pixels back to a category, so
proxy.selectXRangeon a String axis won't fireonChanged.
ChartPlotStyle quick reference
The closure receives an empty ChartPlotProxy and must return a (possibly transformed) ChartPlotProxy. Each chained call returns a new immutable proxy and accumulates an op; the bridge replays the ops on the real ChartPlotContent view inside chartPlotStyle { plot in ... }.
Available builder methods:
Material tokens: 'ultraThin' / 'thin' / 'regular' / 'thick' / 'ultraThick' / 'bar' (suffix Material is also accepted, e.g. 'regularMaterial').
Like
<ChartOverlay>and<ChartGesture>, only the FIRST<ChartPlotStyle>child of a chart is honored. The closure body must remain pure —setStatecalls inside will trigger an infinite chart-rebuild loop. Use it as a pure builder.
Mark Accessibility
Each mark accepts three optional VoiceOver fields directly on its ChartMarkProps:
These work on every mark type (BarMark, LineMark, PointMark, RuleMark, RectangleMark, AreaMark, sectors, ...) and are applied through the same ChartContent.applyModifiers path as foregroundStyle / opacity / etc.
Test on a real device or in Simulator with
Settings → Accessibility → VoiceOver. Swipe on the chart, then swipe right between marks to hear the labels you set.
Pitfalls
ChartOverlayproxy isnullon the very first synchronous render.<ChartOverlay>falls back toEmptyViewuntil SwiftUI has built the chart and injected a real proxy. Build for that case in your render function.SelectedRange / selectedRangeAxisare NOT exposed onChartProxy. SwiftUI Charts does not surface range-selection state throughChartProxy— observe it through thechartXSelection(range:)/chartYSelection(range:)binding instead. The TS interface deliberately omits these.chartOverlaydoes not have aspacingparameter. Onlyalignmentis supported (matches the SwiftUI API).- Keep overlay content cheap. SwiftUI rebuilds the overlay closure every time the chart re-renders. Avoid heavy work or async kick-off inside.
<ChartGesture>/<ChartOverlay>closure body MUST stay pure. SwiftUI Charts re-runs the closure on every chart rebuild; callingsetStateinside the body triggers a React re-render → another chart rebuild → the closure runs again → infinite loop. Push state changes into the gesture'sonChanged / onEndedcallbacks (which are user-event triggered) instead.- The
<ChartGesture>closure must return aGestureInfo(the value returned byDragGesture()/TapGesture()/ etc.). Returningnullor any other type is silently ignored.
